﻿using Server.EventHandlers;
using Server.Packets;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    /// <summary>
    /// A manager managing the <see cref="T:System.Net.Sockets.TcpClient" /> connected to a <see cref="T:Server.TcpServer" />.
    /// </summary>
    public class TcpClientManager : IDisposable
    {
        #region Events

        /// <summary>
        /// Occurs when an error is raised.
        /// </summary>
        private event GenericEventHandler<TcpClientManager, ExceptionEventArgs> m_onError;

        /// <summary>
        /// Occurs when a <see cref="T:Server.Packets.Packet" /> is sent to a client.
        /// </summary>
        private event GenericEventHandler<TcpClientManager, PacketEventArgs> m_onPacketSent;

        #endregion

        #region Fields

        /// <summary>
        /// The number of milliseconds during which the <see cref="T:Server.TcpClientManager" /> will be sleeping when it has no work to do.
        /// </summary>
        private readonly int m_idleTime;

        /// <summary>
        /// The number of milliseconds to wait before the connection with a <see cref="T:System.Net.Sockets.TcpClient" /> is considered to be lost.
        /// </summary>
        private readonly int m_clientTimeout;

        /// <summary>
        /// The <see cref="T:System.Text.Encoding" /> format.
        /// </summary>
        private readonly Encoding m_encoding;

        /// <summary>
        /// The <see cref="T:Server.TcpServer" /> related to this <see cref="T:Server.TcpClientManager" />.
        /// </summary>
        private readonly TcpServer m_server;

        /// <summary>
        /// The list of <see cref="T:Server.Packets.Packet" /> to send to the client.
        /// </summary>
        private readonly ConcurrentQueue<Packet> m_packetsToSend;

        /// <summary>
        /// The managed <see cref="T:System.Net.Sockets.TcpClient" />.
        /// </summary>
        private readonly TcpClient m_client;

        /// <summary>
        /// The packets sender handler task.
        /// </summary>
        private readonly Task m_packetsSenderHandlerTask;

        /// <summary>
        /// The packets receiver handler task.
        /// </summary>
        private readonly Task m_packetsReceiverHandlerTask;

        /// <summary>
        /// Indicates whether the <see cref="T:Server.TcpClientManager" /> is executing the <see cref="M:Server.TcpClientManager.Dispose(System.Boolean)" /> method.
        /// </summary>
        private bool m_isDisposing;

        /// <summary>
        /// The instant of the last time some data was received from the client.
        /// </summary>
        private DateTime m_lastReceivedData;

        /// <summary>
        /// Indicates whether the client manager should receive packets.
        /// </summary>
        private bool m_receivePackets;

        /// <summary>
        /// Indicates whether the client manager should send packets.
        /// </summary>
        private bool m_sendPackets;

        #endregion

        #region Properties

        /// <summary>
        /// The managed client Ip.
        /// </summary>
        /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred when attempting to access the socket -or - The address family is <see cref="F:System.Net.Sockets.AddressFamily.InterNetworkV6" /> and the address is bad.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.Socket"/> has been closed.</exception>
        /// <exception cref="T:System.InvalidOperationException">The remote IP could not be determined.</exception>
        public string Ip
        {
            get
            {
                Contract.Ensures(!String.IsNullOrWhiteSpace(Contract.Result<string>()));
                Contract.Assert(m_client != null);

                return m_client.GetRemoteIp();
            }
        }

        /// <summary>
        /// The <see cref="T:Server.TcpServer" /> related to this <see cref="T:Server.TcpClientManager" />.
        /// </summary>
        public TcpServer Server
        {
            get
            {
                Contract.Ensures(Contract.Result<TcpServer>() != null);

                return m_server;
            }
        }

        /// <summary>
        /// The list of <see cref="T:Server.Packets.Packet" /> to send to the client.
        /// </summary>
        internal ConcurrentQueue<Packet> PacketsToSend
        {
            get
            {
                Contract.Ensures(Contract.Result<ConcurrentQueue<Packet>>() != null);

                return m_packetsToSend;
            }
        }

        /// <summary>
        /// Indicates whether the client is still connected.
        /// </summary>
        internal bool IsConnected
        {
            get { return m_client.Connected && (DateTime.Now - m_lastReceivedData).TotalMilliseconds < m_clientTimeout; }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="T:Server.TcpClientManager" /> class.
        /// </summary>
        /// <param name="server">The <see cref="T:Server.TcpClientManager" /> related with this manager.</param>
        /// <param name="client">The <see cref="T:System.Net.Sockets.TcpClient" /> handled by this manager.</param>
        /// <param name="idleTime">The number of milliseconds during which the <see cref="T:Server.TcpClientManager" /> will be sleeping when it has no work to do.</param>
        /// <param name="clientTimeout">The number of milliseconds to wait before the connection with a client is considered to be lost.</param>
        /// <param name="encoding">The character encoding method.</param>
        /// <param name="onError">The method that will handle the <see cref="T:System.Exception" /> events.</param>
        /// <param name="onPacketSent">The method that will handle a packet sent event.</param>
        internal TcpClientManager(TcpServer server, TcpClient client, int idleTime, int clientTimeout, Encoding encoding, GenericEventHandler<TcpClientManager, ExceptionEventArgs> onError, GenericEventHandler<TcpClientManager, PacketEventArgs> onPacketSent)
        {
            Contract.Requires<ArgumentNullException>(server != null);
            Contract.Requires<ArgumentNullException>(client != null);
            Contract.Requires<ArgumentOutOfRangeException>(idleTime >= 0);
            Contract.Requires<ArgumentOutOfRangeException>(clientTimeout >= 0);
            Contract.Requires<ArgumentNullException>(encoding != null);

            m_server = server;
            m_idleTime = idleTime;
            m_clientTimeout = clientTimeout;
            m_encoding = encoding;
            m_packetsToSend = new ConcurrentQueue<Packet>();
            m_onPacketSent = onPacketSent;
            m_onError = onError;
            m_isDisposing = false;
            m_lastReceivedData = DateTime.Now;

            // set the client timeouts
            m_client = client;
            m_client.ReceiveTimeout = clientTimeout;
            m_client.SendTimeout = clientTimeout;

            // define the receive packet handler
            m_receivePackets = false;
            m_packetsReceiverHandlerTask = new Task(PacketsReceiverHandler);

            // define the send packet handler
            m_sendPackets = false;
            m_packetsSenderHandlerTask = new Task(PacketsSenderHandler);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Define the invariant valid state for this class.
        /// </summary>
        [ContractInvariantMethod]
        private void InvariantValidState()
        {
            Contract.Invariant(m_server != null);
            Contract.Invariant(m_client != null);
            Contract.Invariant(m_idleTime >= 0);
            Contract.Invariant(m_clientTimeout >= 0);
            Contract.Invariant(m_encoding != null);
            Contract.Invariant(m_packetsToSend != null);
            Contract.Invariant(m_packetsReceiverHandlerTask != null);
            Contract.Invariant(m_packetsSenderHandlerTask != null);
        }

        /// <summary>
        /// Start the the <see cref="T:Server.TcpClientManager" />.
        /// </summary>
        internal void Start()
        {
            // start the receive packet handler
            m_receivePackets = true;
            m_packetsReceiverHandlerTask.Start();

            // start the send packet handler
            m_sendPackets = true;
            m_packetsSenderHandlerTask.Start();
        }

        /// <summary>
        /// Handle the <see cref="T:Server.Packets.Packet" /> receiving.
        /// </summary>
        /// <exception cref="T:System.InvalidOperationException">The <see cref="T:System.Net.Sockets.TcpClient" /> is not connected to a remote host.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.TcpClient" /> has been closed.</exception>
        /// <exception cref="T:System.ArgumentException">The <see cref="T:System.Net.Sockets.TcpClient" /> stream does not support writing or is already closed.</exception>
        /// <exception cref="T:System.ArgumentNullException">The <see cref="T:System.Net.Sockets.TcpClient" /> stream is null.</exception>
        /// <exception cref="T:System.IO.EndOfStreamException">The end of the stream is reached.</exception>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="T:System.InvalidOperationException">Unknown packet id -or- the packet could not be instanced.</exception>
        private void PacketsReceiverHandler()
        {
            Contract.Assert(m_client != null);
            Contract.Assert(m_server != null);
            Contract.Assert(m_encoding != null);
            Contract.Assert(m_idleTime >= 0);

            // loop while packets should be received
            while (m_receivePackets)
            {
                try
                {
                    if (m_client.Available > 0)
                    {
                        using (var reader = new BinaryReader(m_client.GetStream(), m_encoding, true))
                        {
                            m_server.ReceivedPackets.Enqueue(new ClientPacket(this, Packet.ReadData(reader)));
                        }
                        m_lastReceivedData = DateTime.Now;
                        Thread.Yield();
                    }
                    else
                        Thread.Sleep(m_idleTime);
                }
                catch (SystemException e)
                {
                    HandleException(e);
                }
            }
        }

        /// <summary>
        /// Handle the <see cref="T:Server.Packets.Packet" /> sending.
        /// </summary>
        /// <exception cref="T:System.Collections.Generic.KeyNotFoundException">A packet type was not mapped.</exception>
        /// <exception cref="T:System.InvalidOperationException">The <see cref="T:System.Net.Sockets.TcpClient" /> is not connected to a remote host.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.TcpClient" /> has been closed.</exception>
        /// <exception cref="T:System.ArgumentException">The <see cref="T:System.Net.Sockets.TcpClient" /> stream does not support writing or is already closed.</exception>
        /// <exception cref="T:System.ArgumentNullException">The <see cref="T:System.Net.Sockets.TcpClient" /> stream is null.</exception>
        private void PacketsSenderHandler()
        {
            Contract.Assert(m_packetsToSend != null);
            Contract.Assert(m_client != null);
            Contract.Assert(m_encoding != null);
            Contract.Assert(m_idleTime >= 0);

            // loop while packets should be sended
            while (m_sendPackets)
            {
                try
                {
                    Packet packet;
                    if (m_packetsToSend.TryDequeue(out packet) && packet != null)
                    {
                        using (var writer = new BinaryWriter(m_client.GetStream(), m_encoding, true))
                        {
                            writer.Write(packet.WriteData(m_encoding));
                        }

                        // raise the OnPacketSent event if defined
                        if (m_onPacketSent != null)
                            Task.Factory.StartNew(() => m_onPacketSent(this, new PacketEventArgs(packet)));

                        Thread.Yield();
                    }
                    else
                        Thread.Sleep(m_idleTime);
                }
                catch (SystemException e)
                {
                    HandleException(e);
                }
            }
        }

        /// <summary>
        /// Handle an <see cref="T:System.Exception" />.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Exception" /> that occured.</param>
        private void HandleException(Exception e)
        {
            // raise the m_onClientError event if defined
            if (m_onError == null)
                throw e;
            Task.Factory.StartNew(() => m_onError(this, new ExceptionEventArgs(e)));
            Thread.Yield();
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Implementation of <see cref="T:System.IDisposable" />.
        /// </summary>
        /// <exception cref="T:System.ObjectDisposedException">A <see cref="T:System.Threading.Tasks.Task" /> has already been disposed.</exception>
        /// <exception cref="T:System.AggregateException">A <see cref="T:System.Threading.Tasks.Task" /> was canceled -or- an exception was thrown during the execution of the <see cref="T:System.Threading.Tasks.Task" />. If the task was canceled, the 
        /// <see cref="T:System.AggregateException" /> contains an <see cref="T:System.OperationCanceledException" /> in its <see cref="P:System.AggregateException.InnerExceptions" /> collection.</exception>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Finalizer.
        /// </summary>
        /// <remarks>Simply calls <code>Dispose(false)</code>.</remarks>
        ~TcpClientManager()
        {
            Dispose(false);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> if managed resources should be disposed; otherwise <c>false</c>.</param>
        /// <exception cref="T:System.ObjectDisposedException">A <see cref="T:System.Threading.Tasks.Task" /> has already been disposed.</exception>
        /// <exception cref="T:System.AggregateException">A <see cref="T:System.Threading.Tasks.Task" /> was canceled -or- an exception was thrown during the execution of the <see cref="T:System.Threading.Tasks.Task" />. If the task was canceled, the 
        /// <see cref="T:System.AggregateException" /> contains an <see cref="T:System.OperationCanceledException" /> in its <see cref="P:System.AggregateException.InnerExceptions" /> collection.</exception>
        protected virtual void Dispose(bool disposing)
        {
            Contract.Assert(m_packetsReceiverHandlerTask != null);
            Contract.Assert(m_packetsSenderHandlerTask != null);
            Contract.Assert(m_idleTime >= 0);
            Contract.Assert(m_client != null);

            // the finalizer didn't calls for this
            if (disposing)
            {
                // prevents concurrent disposing call
                if (m_isDisposing) return;

                m_isDisposing = true;

                // stop the handler tasks
                m_receivePackets = m_sendPackets = false;
                try
                {
                    Parallel.Invoke(() =>
                    {
                        if (m_packetsReceiverHandlerTask.Wait(m_idleTime))
                            m_packetsReceiverHandlerTask.Dispose();
                    }, () =>
                    {
                        if (m_packetsSenderHandlerTask.Wait(m_idleTime))
                            m_packetsSenderHandlerTask.Dispose();
                    });
                }
                catch (ObjectDisposedException e)
                {
                    HandleException(e);
                }
                catch (AggregateException e)
                {
                    HandleException(e);
                }
                finally
                {
                    m_client.Close();
                    m_isDisposing = false;
                }
            }
        }

        #endregion
    }
}
